package graphql.util;


import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import graphql.Internal;
import org.jspecify.annotations.NonNull;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;


@Internal
public class FpKit {

    //
    // From a list of named things, get a map of them by name, merging them according to the merge function
    public static <T> Map<String, T> getByName(List<T> namedObjects, Function<T, String> nameFn, BinaryOperator<T> mergeFunc) {
        return toMap(namedObjects, nameFn, mergeFunc);
    }

    //
    // From a collection of keyed things, get a map of them by key, merging them according to the merge function
    public static <T, NewKey> Map<NewKey, T> toMap(Collection<T> collection, Function<T, NewKey> keyFunction, BinaryOperator<T> mergeFunc) {
        Map<NewKey, T> resultMap = Maps.newLinkedHashMapWithExpectedSize(collection.size());
        for (T obj : collection) {
            NewKey key = keyFunction.apply(obj);
            if (resultMap.containsKey(key)) {
                T existingValue = resultMap.get(key);
                T mergedValue = mergeFunc.apply(existingValue, obj);
                resultMap.put(key, mergedValue);
            } else {
                resultMap.put(key, obj);
            }
        }
        return resultMap;
    }

    // normal groupingBy but with LinkedHashMap
    public static <T, NewKey> Map<NewKey, ImmutableList<T>> groupingBy(Collection<T> list, Function<T, NewKey> function) {
        return filterAndGroupingBy(list, ALWAYS_TRUE, function);
    }

    @SuppressWarnings("unchecked")
    public static <T, NewKey> Map<NewKey, ImmutableList<T>> filterAndGroupingBy(Collection<T> list,
                                                                                Predicate<? super T> predicate,
                                                                                Function<T, NewKey> function) {
        //
        // The cleanest version of this code would have two maps, one of immutable list builders and one
        // of the built immutable lists.  BUt we are trying to be performant and memory efficient so
        // we treat it as a map of objects and cast like its Java 4x
        //
        Map<NewKey, Object> resutMap = new LinkedHashMap<>();
        for (T item : list) {
            if (predicate.test(item)) {
                NewKey key = function.apply(item);
                // we have to use an immutable list builder as we built it out
                ((ImmutableList.Builder<Object>) resutMap.computeIfAbsent(key, k -> ImmutableList.builder()))
                        .add(item);
            }
        }
        if (resutMap.isEmpty()) {
            return Collections.emptyMap();
        }
        // Convert builders to ImmutableLists in place to avoid an extra allocation
        // yes the code is yuck -  but its more performant yuck!
        resutMap.replaceAll((key, builder) ->
                ((ImmutableList.Builder<Object>) builder).build());

        // make it the right shape - like as if generics were never invented
        return (Map<NewKey, ImmutableList<T>>) (Map<?, ?>) resutMap;
    }

    public static <T, NewKey> Map<NewKey, T> toMapByUniqueKey(Collection<T> list, Function<T, NewKey> keyFunction) {
        return toMap(list, keyFunction, throwingMerger());
    }


    private static final Predicate<Object> ALWAYS_TRUE = o -> true;

    private static final BinaryOperator<Object> THROWING_MERGER_SINGLETON = (u, v) -> {
        throw new IllegalStateException(String.format("Duplicate key %s", u));
    };


    private static <T> BinaryOperator<T> throwingMerger() {
        //noinspection unchecked
        return (BinaryOperator<T>) THROWING_MERGER_SINGLETON;
    }


    //
    // From a list of named things, get a map of them by name, merging them first one added
    public static <T> Map<String, T> getByName(List<T> namedObjects, Function<T, String> nameFn) {
        return getByName(namedObjects, nameFn, mergeFirst());
    }

    public static <T> BinaryOperator<T> mergeFirst() {
        return (o1, o2) -> o1;
    }

    /**
     * Converts an object that should be an Iterable into a Collection efficiently, leaving
     * it alone if it is already is one.  Useful when you want to get the size of something
     *
     * @param iterableResult the result object
     * @param <T>            the type of thing
     *
     * @return an Iterable from that object
     *
     * @throws java.lang.ClassCastException if it's not an Iterable
     */
    @SuppressWarnings("unchecked")
    public static <T> Collection<T> toCollection(Object iterableResult) {
        if (iterableResult instanceof Collection) {
            return (Collection<T>) iterableResult;
        }
        Iterable<T> iterable = toIterable(iterableResult);
        Iterator<T> iterator = iterable.iterator();
        List<T> list = new ArrayList<>();
        while (iterator.hasNext()) {
            list.add(iterator.next());
        }
        return list;
    }

    /**
     * Creates an {@link ArrayList} sized appropriately to the collection, typically for copying
     *
     * @param collection the collection of a certain size
     * @param <T>        to two
     *
     * @return a new {@link ArrayList} initially sized to the same as the collection
     */
    public static <T> @NonNull List<T> arrayListSizedTo(@NonNull Collection<?> collection) {
        return new ArrayList<>(collection.size());
    }


    /**
     * Converts a value into a list if it's really a collection or array of things
     * else it turns it into a singleton list containing that one value
     *
     * @param possibleIterable the possible
     * @param <T>              for two
     *
     * @return an list one way or another
     */
    @SuppressWarnings("unchecked")
    public static <T> List<T> toListOrSingletonList(Object possibleIterable) {
        if (possibleIterable instanceof List) {
            return (List<T>) possibleIterable;
        }
        if (isIterable(possibleIterable)) {
            return ImmutableList.copyOf(toIterable(possibleIterable));
        }
        return ImmutableList.of((T) possibleIterable);
    }

    public static boolean isIterable(Object result) {
        return result.getClass().isArray() || result instanceof Iterable || result instanceof Stream || result instanceof Iterator;
    }


    @SuppressWarnings("unchecked")
    public static <T> Iterable<T> toIterable(Object iterableResult) {
        if (iterableResult instanceof Iterable) {
            return ((Iterable<T>) iterableResult);
        }

        if (iterableResult instanceof Stream) {
            return ((Stream<T>) iterableResult)::iterator;
        }

        if (iterableResult instanceof Iterator) {
            return () -> (Iterator<T>) iterableResult;
        }

        if (iterableResult.getClass().isArray()) {
            return () -> new ArrayIterator<>(iterableResult);
        }

        throw new ClassCastException("not Iterable: " + iterableResult.getClass());
    }

    private static class ArrayIterator<T> implements Iterator<T> {

        private final Object array;
        private final int size;
        private int i;

        private ArrayIterator(Object array) {
            this.array = array;
            this.size = Array.getLength(array);
            this.i = 0;
        }

        @Override
        public boolean hasNext() {
            return i < size;
        }

        @SuppressWarnings("unchecked")
        @Override
        public T next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            return (T) Array.get(array, i++);
        }

    }

    public static OptionalInt toSize(Object iterableResult) {
        if (iterableResult instanceof Collection) {
            return OptionalInt.of(((Collection<?>) iterableResult).size());
        }

        if (iterableResult.getClass().isArray()) {
            return OptionalInt.of(Array.getLength(iterableResult));
        }

        return OptionalInt.empty();
    }

    /**
     * Concatenates (appends) a single elements to an existing list
     *
     * @param l   the list onto which to append the element
     * @param t   the element to append
     * @param <T> the type of elements of the list
     *
     * @return a <strong>new</strong> list composed of the first list elements and the new element
     */
    public static <T> List<T> concat(List<T> l, T t) {
        List<T> list = new ArrayList<>(l.size() + 1);
        list.addAll(l);
        list.add(t);
        return list;
    }

    /**
     * Concatenates two lists into one
     *
     * @param l1  the first list to concatenate
     * @param l2  the second list to concatenate
     * @param <T> the type of element of the lists
     *
     * @return a <strong>new</strong> list composed of the two concatenated lists elements
     */
    public static <T> List<T> concat(List<? extends T> l1, List<? extends T> l2) {
        List<T> l = new ArrayList<>(l1.size() + l2.size());
        l.addAll(l1);
        l.addAll(l2);
        return l;
    }

    //
    // quickly turn a map of values into its list equivalent
    public static <T> List<T> valuesToList(Map<?, T> map) {
        return new ArrayList<>(map.values());
    }

    public static <T> List<List<T>> transposeMatrix(List<? extends List<T>> matrix) {
        int rowCount = matrix.size();
        int colCount = matrix.get(0).size();
        List<List<T>> result = new ArrayList<>();
        for (int i = 0; i < rowCount; i++) {
            for (int j = 0; j < colCount; j++) {
                T val = matrix.get(i).get(j);
                if (result.size() <= j) {
                    result.add(j, new ArrayList<>());
                }
                result.get(j).add(i, val);
            }
        }
        return result;
    }

    public static <T> Optional<T> findOne(Collection<T> list, Predicate<T> filter) {
        for (T t : list) {
            if (filter.test(t)) {
                return Optional.of(t);
            }
        }
        return Optional.empty();
    }

    public static <T> T findOneOrNull(List<T> list, Predicate<T> filter) {
        return findOne(list, filter).orElse(null);
    }

    public static <T> int findIndex(List<T> list, Predicate<T> filter) {
        for (int i = 0; i < list.size(); i++) {
            if (filter.test(list.get(i))) {
                return i;
            }
        }
        return -1;
    }

    public static <T> List<T> filterList(Collection<T> list, Predicate<T> filter) {
        List<T> result = arrayListSizedTo(list);
        for (T t : list) {
            if (filter.test(t)) {
                result.add(t);
            }
        }
        return result;
    }

    public static <T> Set<T> filterSet(Collection<T> input, Predicate<T> filter) {
        ImmutableSet.Builder<T> result = ImmutableSet.builder();
        for (T t : input) {
            if (filter.test(t)) {
                result.add(t);
            }
        }
        return result.build();
    }

    /**
     * Used in simple {@link Map#computeIfAbsent(Object, java.util.function.Function)} cases
     *
     * @param <K> for Key
     * @param <V> for Value
     *
     * @return a function that allocates a list
     */
    public static <K, V> Function<K, List<V>> newList() {
        return k -> new ArrayList<>();
    }

    /**
     * This will memoize the Supplier within the current thread's visibility, that is it does not
     * use volatile reads but rather use a sentinel check and re-reads the delegate supplier
     * value if the read has not stuck to this thread.  This means that it's possible that your delegate
     * supplier MAY be called more than once across threads, but only once on the same thread.
     *
     * @param delegate the supplier to delegate to
     * @param <T>      for two
     *
     * @return a supplier that will memoize values in the context of the current thread
     */
    public static <T> Supplier<T> intraThreadMemoize(Supplier<T> delegate) {
        return new IntraThreadMemoizedSupplier<>(delegate);
    }

    /**
     * This will memoize the Supplier across threads and make sure the Supplier is exactly called once.
     * <p>
     * Use for potentially costly actions. Otherwise consider {@link #intraThreadMemoize(Supplier)}
     *
     * @param delegate the supplier to delegate to
     * @param <T>      for two
     *
     * @return a supplier that will memoize values in the context of the all the threads
     */
    public static <T> Supplier<T> interThreadMemoize(Supplier<T> delegate) {
        return new InterThreadMemoizedSupplier<>(delegate);
    }

    /**
     * Faster set intersection.
     *
     * @param <T>  for two
     * @param set1 first set
     * @param set2 second set
     *
     * @return intersection set
     */
    public static <T> Set<T> intersection(Set<T> set1, Set<T> set2) {
        // Set intersection calculation is expensive when either set is large. Often, either set has only one member.
        // When either set contains only one member, it is equivalent and much cheaper to calculate intersection via contains.
        if (set1.size() == 1 && set2.contains(set1.iterator().next())) {
            return set1;
        } else if (set2.size() == 1 && set1.contains(set2.iterator().next())) {
            return set2;
        }

        // Guava's Sets.intersection is faster when the smaller set is passed as the first argument.
        if (set1.size() < set2.size()) {
            return Sets.intersection(set1, set2);
        }
        return Sets.intersection(set2, set1);
    }

}
